使用 Svelte 开发 WebComponents
•
# 使用 Svelte 开发 WebComponents
## Base
- **S****velte3** **https://svelte.dev**
### 完整 Demo
https://github.com/lbb00/svelte-web-compnents-demo
## 为什么使用 Svelte
- 书写更少的代码
- 没有虚拟 DOM,没有运行时,生成的代码体积非常小
其实,使用 Vue3 开发 WebComponents 也可以,可以参考👇🏻的分析:
https://github.com/yyx990803/vue-svelte-size-analysis
## 开发
### 创建项目
#### 创建 Svelte 项目
```shell
npm create vite@latest myapp -- --template svelte
```
#### 在 src/lib 下添加 Button.svelte 和 Loading.svelte 两个组件
```html
<!-- Button.svelte -->
<script>
import { createEventDispatcher } from 'svelte'
import Loading from './Loading.svelte'
const dispatch = createEventDispatcher()
export let text = ''
export let loading = false
function onClick(e) {
dispatch('tap', e.detail)
}
</script>
<button class="custom-button" on:click={onClick}>
{#if loading}
<Loading />
{/if}
<slot>{text}</slot>
</button>
<style>
.custom-button {
font-size: 2rem;
background-color: #256eff;
box-shadow: 0px 15px 27px 2px rgba(37, 110, 255, 0.28);
border: none;
outline: none;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem 3rem;
border-radius: 1rem;
color: #fff;
cursor: pointer;
}
</style
<!-- Loading.svelte -->
<span class="loading" />
<style>
.loading {
display: block;
width: 25px;
height: 25px;
margin-right: 10px;
border-radius: 50%;
border: 3px solid #fff;
border-color: #fff transparent #fff transparent;
animation: ring 1.2s linear infinite;
}
@keyframes ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
```
#### 修改 app.svelte
```html
<script>
import CustomButton from './lib/Button.svelte'
let loading = false
function onTap(e) {
loading = true
setTimeout(() => {
loading = false
}, 2000)
}
</script>
<main>
<CustomButton bind:loading on:tap={onTap}>Start</CustomButton>
</main>
```
#### 运行起来
执行 `npm run dev` 后打开网页会展示下图的按钮,demo 项目就准备好了。
点击 Start ,就会出现 loading 动画。

### 构建为 WebComponents
#### 修改 vite.config.js 和 package.json
需要注意的是,如果组件中一部分作为 WebCopmonents,一部分内部组件不作为 WebCopmonents,将不作为 WebComponents 的组件设置 `<svelte:options tag={null} />` 构建后,使用时会遇到 `Uncaught TypeError: Illegal constructor` 的错误。
参考 https://github.com/sveltejs/svelte/issues/3594 解决该问题。
```javascript
// vite.config.js
import { resolve } from 'node:path'
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
const isLib = process.env.MODE === 'lib'
const dev = defineConfig({
plugins: [svelte()],
})
const lib = defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/lib/Button.svelte'),
name: 'CustomButton',
fileName: 'button',
},
},
plugins: [
svelte({
compilerOptions: {
customElement: true,
},
include: 'src/lib/Button.svelte',
}),
svelte({
compilerOptions: {
customElement: false,
},
// 避免产生独立的 css 文件
emitCss: false,
exclude: 'src/lib/Button.svelte',
}),
],
})
export default isLib ? lib : dev
{
// ...
"scripts": {
// build 时新增 MODE=lib 的 env
"build": "MODE=lib vite build",
}
//...
}
```
#### 为自定义组件添加 <svelte:options tag="..." />
```html
<!-- Button.svelte -->
<svelte:options tag="custom-button" />
```
#### 解决 dispatch 事件不生效
https://github.com/sveltejs/svelte/issues/3119#issuecomment-588566575
把作为 WebComponents 组件中的 dispatch 替换为如下代码
```javascript
import { createEventDispatcher } from 'svelte'
import { get_current_component } from 'svelte/internal'
const component = get_current_component()
const svelteDispatch = createEventDispatcher()
const dispatch = (name, detail) => {
component.dispatchEvent &&
component.dispatchEvent(new CustomEvent(name, { detail }))
return svelteDispatch(name, detail)
}
```
#### Props
Svelte 构建的 WebComponents 组件无法将属性类似 'foo-bar' 转化为 'fooBar' 的 props, 因此需要时用 'foobar' 或 'foo_bar' 作为 props。
#### 构建
执行 `npm run build`
### 在其他框架中使用 WebCompnents 组件
#### Vue
可以查看 demo 项目中的 demo.vue.html
```html
<!DOCTYPE html>
<html>
<body>
<div id="app">
<custom-button :loading="loading" @tap="onTap">Start</custom-button>
</div>
</body>
<script src="./dist/button.umd.cjs"></script>
<script type="module">
import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
createApp({
setup() {
const loading = ref(false)
function onTap(e) {
loading.value = true
setTimeout(() => {
loading.value = false
}, 2000)
}
return {
loading,
onTap,
}
},
}).mount('#app')
</script>
</html>
```
#### React
可以查看 demo 项目中的 demo.react.html
```html
<!DOCTYPE html>
<html>
<div id="app"></div>
<body>
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="./dist/button.umd.cjs"></script>
<script type="text/babel">
const Demo = () => {
// loading 不能设置为 false,因为会作为字符串’false'传递给组件
const [loading, setLoading] = React.useState('')
const ref = React.useRef()
React.useLayoutEffect(() => {
const onTap = (e) => {
setLoading(true)
setTimeout(() => {
setLoading('')
}, 2000)
}
const { current } = ref
current.addEventListener('tap', onTap)
return () => {
current.removeEventListener('tap', onTap)
}
}, [ref])
return (
<custom-button loading={loading} ref={ref}>
Start
</custom-button>
)
}
const container = document.getElementById('app')
const root = ReactDOM.createRoot(container)
root.render(<Demo />)
</script>
</body>
</html>
```